import Axios from 'axios'
import Qs from 'qs'
import { Dialog, Notify } from 'quasar'

/**
 * 路径参数占位符
 * @type {string}
 */
const PATH_VAR_PLACEHOLDER = '{}'

/**
 * 没有请求体的 HTTP 请求方法
 * @type {[String]}
 */
const REQUEST_METHOD_WITHOUT_BODY = ['GET', 'HEAD', 'DELETE', 'OPTIONS']

/**
 * 替换 URL 中的路径参数
 * @param url 带有路径参数占位符 (e.g. "{}") 的 URL
 * @param pathVars 用于替换占位符的路径参数数组, 其数量必须与 URL 中占位符的数量一致, 否则将抛出异常
 * @returns {string} 替换后的 URL
 */
function replaceUrlWithPathVars (url, pathVars) {
  let urlFragments = url.split(PATH_VAR_PLACEHOLDER)

  // 检查 placeholder 数量与传入的 path var 数量是否一致
  let expectedCount = urlFragments.length - 1
  if (expectedCount !== pathVars.length) {
    throw new Error('期望传入 ' + expectedCount + ' 个路径参数，但传入了 ' + pathVars.length +
      ' 个路径参数')
  }

  // 无 path var，直接返回 URL
  if (urlFragments.length === 1) {
    return url
  }

  // 依次替换 path var
  pathVars.forEach((pathVar, i) => {
    urlFragments[i] += pathVar
  })

  return urlFragments.join('')
}

/**
 * 显示错误 toast
 * @param err Error 对象
 */
function toastError (err) {
  const selfRepairableSuffix = '，请稍后再次尝试。多次尝试失败后，请联系您的系统管理员。'
  const definiteErrorSuffix = '，请立即联系您的系统管理员。'
  let message

  // noinspection MagicNumberJS
  switch (err.response.status) {
    case 400:
    case 404:
    case 405:
    case 413:
    case 414:
    case 415:
      message = '请求错误' + definiteErrorSuffix
      break
    case 500:
      message = '服务器错误' + definiteErrorSuffix
      break
    case 502:
    case 504:
      message = '网关暂无响应' + selfRepairableSuffix
      break
    default:
      message = '罕见错误' + definiteErrorSuffix
  }

  Notify.create({
    position: 'top',
    icon: 'warning',
    color: 'negative',
    timeout: 2500,
    multiLine: true,
    actions: [
      { label: '详情', color: 'white', handler: () => showErrorDetail(err) },
      { label: '关闭', color: 'white' }
    ],
    message: message
  })
}

/**
 * 显示错误详情
 * @param err Error 对象
 */
function showErrorDetail (err) {
  Dialog.create({
    title: '错误详情',
    style: 'white-space: pre;',
    message: err.stack || err.message,
    persistent: true,
    fullWidth: true
  })
}

/**
 * API Fetcher
 */
export class ApiFetcher {
  /**
   * constructor
   * @param [extraAxiosConfig = {}] 额外的 Axios 配置，将与默认配置融合，而不是替换默认配置
   */
  constructor (extraAxiosConfig = {}) {
    // 缺省 QS module 配置
    const defaultQsConfig = {
      arrayFormat: 'brackets',
      skipNulls: true,
      format: 'RFC3986'
    }

    // 缺省 Axios 配置
    const defaultAxiosConfig = {
      baseURL: process.env.API_BASE_URL,
      timeout: 5000,
      paramsSerializer: function (params) {
        return Qs.stringify(params, defaultQsConfig)
      }
    }

    // Axios 实例
    const mergedAxiosConfig = Object.assign({}, defaultAxiosConfig, extraAxiosConfig)
    this.axiosInstance = Axios.create(mergedAxiosConfig)

    // Axios 响应拦截器
    this.axiosInstance.interceptors.response.use(response => {
      return response.data
    }, err => {
      toastError(err)
      return Promise.reject(err)
    })
  }

  /**
   * 发送 GET 请求
   * 请求参数会被处理为 query string
   * @example
   * let url = '/factories/{}/departments/{}'
   * let reqData = {foo: 'bar'}
   * apiFetcher.get(url, 1, 2, reqData)
   * @param url 请求 URL
   * <p>
   * 对于 URL 中的路径参数, 使用 {} 替代, e.g. http://api.example.com/factories/{}/departments/{}
   * @param args 剩余参数, 顺序依次为: [0, n) 个路径参数, [0, 1] 个请求参数
   * @returns {Promise<*>} promise
   */
  get (url, ...args) {
    return this._request(url, 'GET', ...args)
  }

  /**
   * 发送 POST 请求
   * 请求参数会被处理为 request body
   * @example
   * let url = '/factories/{}/departments'
   * let reqData = {foo: 'bar'}
   * apiFetcher.post(url, 1, reqData)
   * @param url 请求 URL
   * <p>
   * 对于 URL 中的路径参数, 使用 {} 替代, e.g. http://api.example.com/factories/{}/departments/{}
   * @param args 剩余参数, 顺序依次为: [0, n) 个路径参数, [0, 1] 个请求参数
   * @returns {Promise<*>} promise
   */
  post (url, ...args) {
    return this._request(url, 'POST', ...args)
  }

  /**
   * 发送 PUT 请求
   * 请求参数会被处理为 request body
   * @example
   * let url = '/factories/{}/departments/{}'
   * let reqData = {foo: 'bar'}
   * apiFetcher.put(url, 1, 2, reqData)
   * @param url 请求 URL
   * <p>
   * 对于 URL 中的路径参数, 使用 {} 替代, e.g. http://api.example.com/factories/{}/departments/{}
   * @param args 剩余参数, 顺序依次为: [0, n) 个路径参数, [0, 1] 个请求参数
   * @returns {Promise<*>} promise
   */
  put (url, ...args) {
    return this._request(url, 'PUT', ...args)
  }

  /**
   * 发送 PATCH 请求
   * 请求参数会被处理为 request body
   * @example
   * let url = '/factories/{}/departments/{}'
   * let reqData = {foo: 'bar'}
   * apiFetcher.patch(url, 1, 2, reqData)
   * @param url 请求 URL
   * <p>
   * 对于 URL 中的路径参数, 使用 {} 替代, e.g. http://api.example.com/factories/{}/departments/{}
   * @param args 剩余参数, 顺序依次为: [0, n) 个路径参数, [0, 1] 个请求参数
   * @returns {Promise<*>} promise
   */
  patch (url, ...args) {
    return this._request(url, 'PATCH', ...args)
  }

  /**
   * 发送 DELETE 请求
   * 请求参数会被处理为 query string
   * @example
   * let url = '/factories/{}/departments/{}'
   * let reqData = {foo: 'bar'}
   * apiFetcher.delete(url, 1, 2, reqData)
   * @param url 请求 URL
   * <p>
   * 对于 URL 中的路径参数, 使用 {} 替代, e.g. http://api.example.com/factories/{}/departments/{}
   * @param args 剩余参数, 顺序依次为: [0, n) 个路径参数, [0, 1] 个请求参数
   * @returns {Promise<*>} promise
   */
  delete (url, ...args) {
    return this._request(url, 'DELETE', ...args)
  }

  /**
   * 发送请求
   * 对于有请求体的请求方式, 请求参数会被处理为 request body; 对于其他请求方式, 处理为 query string
   * @example
   * let url = '/factories/{}/departments/{}'
   * let reqData = { foo: 'bar' }
   * apiFetcher._request(url, 'PUT', 1, 2, reqData)
   * @param url 请求 URL
   * <p>
   * 对于 URL 中的路径参数, 使用 {} 替代, e.g. http://api.example.com/factories/{}/departments/{}
   * @param method 请求方法, 必须大写, e.g. GET
   * @param args 剩余参数, 顺序依次为: [0, n) 个路径参数, [0, 1] 个请求参数
   * @returns {Promise<*>} promise
   * @private
   */
  _request (url, method, ...args) {
    // 检查传入的 path var 数量是否与 URL 中占位符的数量是否相符
    let expectedPathVarCount = url.split(PATH_VAR_PLACEHOLDER).length - 1
    if (expectedPathVarCount > args.length) {
      throw new Error('期望传入 ' + expectedPathVarCount + ' 个路径参数，但传入了 ' +
        args.length + ' 个路径参数')
    }

    // 从 args 中分离出 path vars 和 req data
    let hasOtherArgs = args.length !== expectedPathVarCount
    let pathVars = hasOtherArgs ? args.slice(0, expectedPathVarCount) : args
    let reqData = hasOtherArgs ? args.slice(expectedPathVarCount, expectedPathVarCount + 1)[0]
      : undefined

    // 若 URL 中有占位符，用路径参数替换之
    let replacedUrl = expectedPathVarCount > 0 ? replaceUrlWithPathVars(url, pathVars) : url

    // 辨识请求方法是否应包含请求体
    let hasBody = !REQUEST_METHOD_WITHOUT_BODY.includes(method)

    // 发送请求
    return this.axiosInstance.request({
      url: replacedUrl,
      method: method,
      params: hasBody ? undefined : reqData,
      data: hasBody ? reqData : undefined,
      headers: {
        // Authorization header
        ...(localStorage.hasOwnProperty('token') && {
          'Authorization': 'Bearer ' + localStorage.getItem('token')
        })
      }
    })
  }
}

export default new ApiFetcher()
